﻿using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.ServiceProcess;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using log4net;
using log4net.Config;
using OrbitOne.Elmah.Agent.ElmahWebService;
using OrbitOne.Elmah.Logic;

namespace OrbitOne.Elmah.Agent
{
    public sealed partial class AgentService : ServiceBase
    {
        private readonly ILog Log = LogManager.GetLogger(typeof(AgentService));

        #region Private members
        private bool _isServiceReachable = true;
        private object _lockMe = new object();
        private int _heartbeatInterval;
        private Timer _heartbeat;
        private Thread _heartbeatThread;
        private Service _webService;
        private AgentConfiguration _config;
        private bool _processing = false;
        #endregion

        #region Constructor
        public AgentService()
        {
            InitializeComponent();
        }
        #endregion

        #region Service Overrides
        protected override void OnStart(string[] args)
        {
            FileInfo fileInfo = new FileInfo(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
            XmlConfigurator.ConfigureAndWatch(fileInfo);
            Log.Info("Service Started");
            try
            {
                _config = AgentConfiguration.Load();

                _webService = new Service();
                _webService.Url = _config.ElmahWebServiceUrl;
                Log.Debug("Connected to " + _webService.Url);
                _heartbeatInterval = _webService.GetHeartBeatInterval();
                _heartbeatThread = new Thread(StartHeartbeat);
                _heartbeatThread.IsBackground = false;
                _heartbeatThread.Start();
            }
            catch (Exception e)
            {
                Log.Fatal("Service failed to start, check Event Viewer for detailed info.");
                EventLog.WriteEntry("Service failed to start. " + e, EventLogEntryType.Error, 5000);
            }
        }

        protected override void OnPause()
        {
            Heartbeat_Pause();
            base.OnPause();
        }

        protected override void OnContinue()
        {
            Heartbeat_Continue();
            base.OnContinue();
        }

        protected override void OnStop()
        {
            _heartbeatThread.Abort();
            _heartbeat.Dispose();
        }
        #endregion

        #region FileSystem Methods
        private void ProcessFiles(object o)
        {
            Log.Debug("Processing files");
            _processing = true;
            lock (_lockMe)
            {
                Log.Info(string.Format("Service is {0}reachable", _isServiceReachable ? "" : "un"));
                if (!_isServiceReachable)
                {
                    _processing = false;
                    return;
                }

                foreach (var file in Directory.GetFiles(_config.ErrorXmlSourceFolder, _config.ErrorXmlFileFilter))
                {
                    Log.Debug(string.Format("Waiting to process file {0}", file));
                    WaitForFile(file);

                    try
                    {
                        Log.Debug(string.Format("Parsing file {0}", file));
                        ParseFile(file);
                    }
                    catch (WebException e)
                    {
                        Log.Error(string.Format("Parsing of file {0} failed", file), e);
                        break;
                    }
                    catch (Exception e)
                    {
                        try
                        {
                            var filename = file.Substring(file.LastIndexOfAny("\\".ToCharArray()));
                            File.Move(file, _config.InvalidLogFileFolder + filename);
                            Log.Debug(string.Format("Invalid Error Xml format: {0}. File {1} is moved to error folder.",
                                                                   e.Message,
                                                                   file));
                        }
                        catch (Exception innerE)
                        {
                            Log.Error(string.Format("Invalid ErrorXml format: {0}. {1} is not moved because {2}",
                                                                    e.Message,
                                                                    file,
                                                                    innerE.Message));
                        }
                        continue;
                    }
                }
            }
            _processing = false;
        }

        private void ParseFile(string file)
        {
            var doc = new XmlDocument();
            doc.Load(file);
            doc.InnerXml = StripIllegalXMLChars(doc.InnerXml, "1.0");

            var errorId = new Guid();
            var nodes = doc.SelectNodes("/error/@errorId");
            errorId = new Guid(nodes[0].InnerXml);

            var logStatus = (LogStatus)_webService.LogError(doc.OuterXml, errorId);
            if (logStatus == LogStatus.Succeed || logStatus == LogStatus.AlreadyLogged)
            {
                File.Delete(file);
                Log.Debug(string.Format("File {0} is processed successfully.", file));
            }
            else
            {
                var filename = file.Substring(file.LastIndexOfAny("\\".ToCharArray()));
                File.Move(file, _config.InvalidLogFileFolder + filename);
                Log.Error(string.Format("Cannot log {0} to Elmah Web Service, check Elmah Web Service log for detail.",
                                         file));
            }
        }

        private static void WaitForFile(string file)
        {
            FileInfo fi;
            var canOpen = false;
            while (canOpen == false)
            {
                try
                {
                    fi = new FileInfo(file);
                    using (fi.OpenWrite())
                    {
                        canOpen = true;
                    }
                }
                catch
                {
                    canOpen = false;
                    Thread.Sleep(10);
                }
            }
        }
        /// <summary>
        /// Strips non-printable ascii characters 
        /// Refer to http://www.w3.org/TR/xml11/#charsets for XML 1.1
        /// Refer to http://www.w3.org/TR/2006/REC-xml-20060816/#charsets for XML 1.0
        /// </summary>
        /// <param name="xml">XML string</param>
        /// <param name="XMLVersion">XML Specification to use. Can be 1.0 or 1.1</param>
        private string StripIllegalXMLChars(string xml, string XMLVersion)
        {
            //Remove illegal character sequences
            string pattern = String.Empty;
            switch (XMLVersion)
            {
                case "1.0":
                    pattern = @"#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|7F|8[0-46-9A-F]9[0-9A-F])";
                    break;
                case "1.1":
                    pattern = @"#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|[19][0-9A-F]|7F|8[0-46-9A-F]|0?[1-8BCEF])";
                    break;
                default:
                    throw new Exception("Error: Invalid XML Version!");
            }

            Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
            if (regex.IsMatch(xml))
            {
                xml = regex.Replace(xml, String.Empty);
            }
            return xml;
        }

        #endregion

        #region Heartbeat Methods
        private void StartHeartbeat(object o)
        {
            _heartbeat = new Timer(Heartbeat_Tick, null, 0, _heartbeatInterval);
        }

        private void Heartbeat_Pause()
        {
            _heartbeat = null;
        }

        private void Heartbeat_Continue()
        {
            _heartbeat = new Timer(Heartbeat_Tick, null, 0, _heartbeatInterval);
        }

        private void Heartbeat_Tick(object o)
        {
            try
            {
                if (!_processing)
                    ThreadPool.QueueUserWorkItem(ProcessFiles);

                _webService.SetHeartbeat(new HeartbeatRequest
                    {
                        Current = DateTime.Now,
                        CreatedOn = _config.AgentCreatedOn,
                        Name = _config.AgentName,
                        Description = _config.AgentDescription
                    });
                Log.Debug("POEDOEM");
            }
            catch (Exception)
            {
                _isServiceReachable = false;
                Log.Info("Webservice not available at the moment. Will try again in 1 minute...");
                Heartbeat_Pause();
                Thread.Sleep(60000);
                _isServiceReachable = true;
                Heartbeat_Continue();
            }
        }
        #endregion

        internal void Start()
        {
            OnStart(null);
        }
    }
}
